package com.xci.sys.service;

import cn.hutool.core.date.DateUtil;
import com.github.pagehelper.Page;
import com.xci.core.annotation.DataScope;
import com.xci.core.annotation.Log;
import com.xci.core.annotation.Valid;
import com.xci.core.async.AsyncManager;
import com.xci.core.base.BaseService;
import com.xci.core.base.RestMessage;
import com.xci.core.domain.*;
import com.xci.core.exceptions.AppException;
import com.xci.core.helper.CacheHelper;
import com.xci.core.helper.DateHelper;
import com.xci.core.helper.Helper;
import com.xci.core.helper.SecurityHelper;
import com.xci.core.internal.Const;
import com.xci.core.internal.Sys;
import com.xci.sys.dao.RoleModuleMapDao;
import com.xci.sys.dao.UserDao;
import com.xci.sys.entity.SysSecurityUser;
import com.xci.sys.entity.SysUser;
import com.xci.sys.filter.UserFilter;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Param;
import org.springframework.cache.Cache;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
import java.util.List;
import java.util.TimerTask;

/**
 * 用户服务
 */
@Slf4j
@Service
public class UserService extends BaseService {
    @Resource
    private UserDao userDao;//用户数据层对象
    @Resource
    private Cache userCache;//用户缓存对象
    @Resource
    private RoleModuleMapDao roleModuleMapDao;//角色模块关联数据层对象
    @Resource
    private OnlineUserService onlineUserService;//在线用户服务
    @Resource
    private LockUserService lockUserService;//锁定用户服务
    @Resource
    private ModuleService moduleService;//模块服务
    @Resource
    private LoginLogService loginLogService;//登陆日志服务
    @Resource
    private Cache captchaCache;//用户验证码缓存

    // region 账号相关

    /**
     * 用户登录
     * @param account  账号
     * @param password 密码
     * @param captcha  验证码
     * @return 登录成功返回true
     */
    public RestMessage<SysUser> login(@NotBlank(message = "请输入账号") String account, @NotBlank(message = "请输入密码") String password, String captcha) {
        if (lockUserService.isLock(account)) {
            LockUserModel lockUserModel = lockUserService.get(account);
            return RestMessage.fail(Helper.format("账号密码输入错误次数达到上限,暂时被锁定,将于 {} 解锁",
                    DateHelper.formatDateHasMinute(lockUserModel.getDisLockTime())));
        }

        if (lockUserService.requireCaptcha(account)) {
            String code = captchaCache.get(account, String.class);
            if ((Helper.isBlank(captcha)) || (Helper.isBlank(code)) || (!code.equalsIgnoreCase(captcha))) {
                captchaCache.evict(account);
                return RestMessage.fail("输入的验证码无效");
            }
        }
        captchaCache.evict(account);

        RestMessage<SysUser> result = valid(account, password);
        if (result.isSuccess()) {
            lockUserService.remove(account);
            SysUser user = result.getData();

            OnlineUserModel onlineUser = onlineUserService.get(user.getId());
            if (!Sys.setting().getApp().isSysUserAllowRepeatLogin() && onlineUser != null) {
                var msg = Helper.format("用户在 {} 在IP:{} 已经登录,系统不允许重复登录!",
                        DateHelper.formatDateTime(onlineUser.getLoginTime()), onlineUser.getIpAddress());
                AsyncManager.me().execute(saveLoginLog(account, false, msg));
                return RestMessage.fail(msg);
            }

            //region 更新账号登录状态
            Date firstVisitDateTime = null;
            Date lastVisitDateTime = new Date();
            if (user.getFirstVisitTime() == null) {
                firstVisitDateTime = new Date();
            }
            userDao.updateLoginStatus(user.getId(), firstVisitDateTime, lastVisitDateTime);
            SysUser cacheUser = CacheHelper.getCacheValue(userCache, user.getId());
            if (cacheUser != null) {
                cacheUser.setFirstVisitTime(firstVisitDateTime);
                cacheUser.setLastVisitTime(lastVisitDateTime);
                cacheUser.setLoginCount(cacheUser.getLoginCount() + 1);
            }
            //endregion

            onlineUserService.active(user); //激活在线用户
            moduleService.clearUserModuleCache(user.getId());//清除用户权限缓存
            String token = Sys.me().buildUserToken(user.getId());
            user.setToken(token);
            AsyncManager.me().execute(saveLoginLog(account, true, "登陆成功"));
            return result;
        } else {
            lockUserService.add(account);
            int limitCount = lockUserService.limtCount(account);
            if (limitCount == 0) {
                var msg = Helper.format("账号{}多次登录失败被锁定", account);
                AsyncManager.me().execute(saveLoginLog(account, false, msg));
            }
            String msg = limitCount > 0 ? Helper.format("{},还可以重试{}次", result.getMsg(),
                    limitCount) : "账号错误次数达到上限,暂时被锁定";
            return RestMessage.fail(msg);
        }
    }

    /**
     * 用户注销
     * @param userId 用户主键
     */
    public RestMessage logout(String userId) {
        var user = selectById(userId);
        if (user != null) {
            onlineUserService.logoff(userId);//注销在线用户
            moduleService.clearUserModuleCache(userId);//清除用户权限缓存
            AsyncManager.me().execute(saveLoginLog(user.getAccount(), true, "注销成功"));
            return RestMessage.success();
        }
        return RestMessage.success();//错误的用户注解,不做任何处理
    }

    /**
     * 验证用户账号和密码
     * @param account  账号
     * @param password 密码
     * @return 验证成功返回true
     */
    public RestMessage<SysUser> valid(@NotBlank(message = "请输入账号") String account, @NotBlank(message = "请输入密码") String password) {
        //region 检测密码强度
        RestMessage result = SecurityHelper.validPwdStrong(password);
        if (result.isFail()) {
            return RestMessage.fail(result.getMsg());
        }
        //endregion

        account = account.trim();
        SysSecurityUser user = userDao.selectSecurityByAccount(account);

        //region 检测条件

        if (user == null) {
            return RestMessage.fail("账号密码错误");
        }

        if (Helper.isBlank(user.getPwdSalt())) {
            return RestMessage.fail("无效的账号");
        }
        String encryptedPwd = SecurityHelper.encryptPassword(account, password, user.getPwdSalt());
        if (!user.getPassword().equals(encryptedPwd)) {
            return RestMessage.fail("账号密码错误");
        }

        if (user.getIsExpire()) {
            return RestMessage.fail("账号已过期");
        }

        if (!user.getStatus()) {
            return RestMessage.fail("账号已被禁用");
        }
        var msg = Const.EMPTY;
        if (!user.getPwdNeverExpire() && user.getPwdExpireTime() != null) {
            int days = Long.valueOf(DateUtil.betweenDay(user.getPwdExpireTime(), new Date(), true)).intValue();
            if (days <= 0) {
                return RestMessage.fail("密码已过期");
            } else if (days <= Sys.setting().getApp().getSysUserPwdExpireRemindDays()) {
                msg = Helper.format("密码将于 {} 天后过期,请及时修改密码.", days);
            }
        }

        //endregion

        return RestMessage.success(msg, selectById(user.getId()));
    }

    /**
     * 修改用户密码
     * @param id              用户主键
     * @param currentPassword 当前密码
     * @param newPassword     新密码
     */
    @Log(module = Const.Module.User, msg = "修改用户密码")
    @Transactional(rollbackFor = Exception.class)
    public RestMessage modifyPassword(@NotBlank(message = "请指定用户主键") String id,
                                      @NotBlank(message = "请指定当前密码") String currentPassword, @NotBlank(message = "请指定新密码") String newPassword) {
        //验证用户主键
        SysSecurityUser entity = userDao.selectSecurityById(id);
        if (entity == null) {
            return RestMessage.fail("无效的用户主键");
        }
        if (entity.getPwdNotAllowedModify()) {
            return RestMessage.fail("不允许用户修改密码");
        }

        //验证密码强度
        RestMessage result = SecurityHelper.validPwdStrong(newPassword);
        if (result.isFail()) {
            return RestMessage.fail(result.getMsg());
        }

        //验证当前密码
        if (!entity.getPassword().equals(
                SecurityHelper.encryptPassword(entity.getAccount(), currentPassword, entity.getPwdSalt()))) {
            return RestMessage.fail("当前密码输入不正确,请重新输入");
        }

        //生成新的密钥和密码
        String account = entity.getAccount();
        String salt = SecurityHelper.createSalt();
        String uPassword = SecurityHelper.encryptPassword(account, newPassword, salt);

        //密码过期时间
        if (entity.getPwdNeverExpire()) {
            entity.setPwdExpireTime(null);
        } else {
            entity.setPwdExpireTime(DateUtil.offsetDay(new Date(), Sys.setting().getApp().getSysUserPwdAvailableDays()));
        }

        userDao.modifyPassword(id, salt, uPassword, entity.getPwdExpireTime());
        userCache.evict(id);
        return RestMessage.success();
    }

    /**
     * 重置用户登录密码
     * @param ids         用户主键数组
     * @param newPassword 新密码
     */
    @Log(module = Const.Module.User, msg = "重置用户密码")
    @Transactional(rollbackFor = Exception.class)
    public RestMessage revisePassword(@NotBlank(message = "请指定用户主键字符串,多个用逗号隔开") String ids,
                                      @NotBlank(message = "请指定新密码") String newPassword) {
        String[] idList = Helper.splitToArray(ids);
        //验证密码强度
        RestMessage result = SecurityHelper.validPwdStrong(newPassword);
        if (result.isFail()) {
            return RestMessage.fail(result.getMsg());
        }

        for (String id : idList) {
            SysSecurityUser user = userDao.selectSecurityById(id);
            if (user == null) {
                continue;
            }
            String account = user.getAccount();
            String salt = SecurityHelper.createSalt();
            String nPassword = SecurityHelper.encryptPassword(account, newPassword, salt);

            userDao.revisePassword(id, salt, nPassword);
            userCache.evict(id);
            // String msg = Helper.format("修改用户密码 主键: {} 账号: {}", id, account);
        }
        return RestMessage.success();
    }

    // endregion

    // region 增删改查

    /**
     * 新建用户
     * @param entity 用户对象
     */
    @Log(module = Const.Module.User, msg = "新建用户")
    @Transactional(rollbackFor = Exception.class)
    public RestMessage insert(@Valid SysUser entity) {
        return save(entity, true);
    }

    /**
     * 修改用户
     * @param entity 用户对象
     */
    @Log(module = Const.Module.User, msg = "修改用户")
    @Transactional(rollbackFor = Exception.class)
    public RestMessage update(@Valid SysUser entity) {
        return save(entity, false);
    }

    /**
     * 批量保存用户对象
     * @param entities 用户对象集合
     */
    @Log(module = Const.Module.Param, msg = "批量保存用户对象")
    @Transactional(rollbackFor = Exception.class)
    public RestMessage batchSave(@Valid List<SysUser> entities) {
        for (SysUser user : entities) {
            if (Helper.isBlank(user.getId())) {   //无主键,则新增
                save(user, true).ifFailException();
            } else if (!userDao.existById(user.getId())) {//主键不存在,则新增
                save(user, true).ifFailException();
            } else {//否则修改
                save(user, false).ifFailException();
            }
        }
        return RestMessage.success();
    }

    /**
     * 修改用户状态
     * @param ids    用户主键字符串,多个逗号隔开
     * @param status 用户状态
     */
    @Log(module = Const.Module.User, msg = "修改用户状态")
    @Transactional(rollbackFor = Exception.class)
    public RestMessage updateStatus(@NotBlank(message = "请指定用户主键") String ids, @NotNull(message = "请指定用户状态") Boolean status) {
        String[] idList = Helper.splitToArray(ids);
        for (String id : idList) {
            userDao.updateStatus(id, status);
            //移除缓存
            userCache.evict(id);
        }
        return RestMessage.success();
    }

    /**
     * 删除用户
     * @param ids 用户主键字符串,多个逗号隔开
     */
    @Log(module = Const.Module.User, msg = "删除用户")
    @Transactional(rollbackFor = Exception.class)
    public RestMessage delete(@NotBlank(message = "请指定用户主键字符串") String ids) {
        SysUser currentUser = getCurrentUser();
        String[] idList = Helper.splitToArray(ids);
        for (String id : idList) {
            SysUser old = userDao.selectById(id);
            if (old == null) throw new AppException("删除用户失败,无效的用户主键:" + id);
            //检测当前账号是否是自身,不允许自己删除自己
            if (old.getId().equals(currentUser.getId()))
                throw new AppException(Helper.format("账号:[{}]删除失败,不允许删除自己", old.getAccount()));
            userDao.delete(id, Helper.nextIdStr()); //根据主键删除用户
            userCache.evict(old.getId());//移除缓存
            saveDeleteHistory(HistoryInfo.builder().pk(id).before(old).build()); //记录历史记录
        }
        return RestMessage.success();
    }

    /**
     * 检查用户主键唯一性
     * @param id 用户主键
     * @return 如果存在返回true, 否则返回false
     */
    public Boolean existById(@NotBlank(message = "请指定用户主键") String id) {
        return userDao.existById(id);
    }

    /**
     * 检查用户账号唯一性
     * @param account 用户账号
     * @return 如果存在返回true, 否则返回false
     */
    public Boolean existByAccount(@NotBlank(message = "请指定用户账号") String account) {
        return userDao.existByAccount(account, null);
    }

    /**
     * 验证是否存在指定机构主键的用户
     * @param deptId 机构主键
     * @return 如果存在返回true, 否则返回false
     */
    public Boolean existByDeptId(@NotBlank(message = "请指定机构主键") String deptId) {
        return userDao.existByDeptId(deptId);
    }

    /**
     * 验证是否存在指定角色主键的用户
     * @param roleId 角色主键
     * @return 如果存在返回true, 否则返回false
     */
    public Boolean existByRoleId(@NotBlank(message = "请指定角色主键") String roleId) {
        return userDao.existByRoleId(roleId);
    }

    /**
     * 查询单个用户
     * @param id 用户主键
     * @return 返回用户对象
     */
    public SysUser selectById(@NotBlank(message = "请指定用户主键") String id) {
        return CacheHelper.getCacheValue(userCache, id, () -> userDao.selectById(id));
    }

    /**
     * 根据户账号查询单个用户
     * @param account 用户账号
     * @return 返回用户对象
     */
    public SysUser selectByAccount(@NotBlank(message = "请指定用户账号") String account) {
        return userDao.selectByAccount(account);
    }

    /**
     * 查询单个用户,包含安全信息
     * @param id 用户主键
     * @return 返回用户对象
     */
    public SysSecurityUser selectSecurityById(@NotBlank(message = "请指定用户主键") String id) {
        return userDao.selectSecurityById(id);
    }

    /**
     * 根据户账号查询单个用户,包含安全信息
     * @param account 用户账号
     * @return 返回用户对象
     */
    public SysSecurityUser selectSecurityByAccount(@NotBlank(message = "请指定用户账号") String account) {
        return userDao.selectSecurityByAccount(account);
    }

    /**
     * 查询简单用户列表
     * @param filter 过滤对象
     * @return 简单用户列表
     */
    @DataScope
    public List<SysUserSimple> selectSimpleList(UserFilter filter) {
        return userDao.selectSimpleList(filter);
    }

    /**
     * 查询简单用户分页列表
     * @param filter 过滤对象
     * @return 简单用户分页列表
     */
    @DataScope
    public Page<SysUserSimple> selectSimplePageList(UserFilter filter) {
        return userDao.selectSimplePageList(filter);
    }

    /**
     * 查询用户列表
     * @param filter 过滤对象
     * @return 返回用户对象列表
     */
    @DataScope
    public List<SysUser> selectList(UserFilter filter) {
        return userDao.selectList(filter);
    }

    /**
     * 查询用户分页列表
     * @param filter 过滤对象
     * @return 返回用户对象分页列表
     */
    @DataScope
    public Page<SysUser> selectPageList(UserFilter filter) {
        return userDao.selectPageList(filter);
    }

    /**
     * 根据机构查询用户成员列表
     * @param deptId 机构主键
     * @return 返回用户对象列表
     */
    public List<SysUser> selectListByDeptId(String deptId) {
        return userDao.selectListByDeptId(deptId);
    }

    /**
     * 根据机构查询用户成员分页列表
     * @param deptId 机构主键
     * @return 返回用户对象分页列表
     */
    public Page<SysUser> selectPageListByDeptId(String deptId) {
        return userDao.selectPageListByDeptId(deptId);
    }

    /**
     * 根据角色查询用户成员列表
     * @param roleId 角色主键
     * @return 返回用户对象列表
     */
    public List<SysUser> selectListByRoleId(String roleId) {
        return userDao.selectListByRoleId(roleId);
    }

    /**
     * 根据角色查询用户成员分页列表
     * @param roleId 角色主键
     * @return 返回用户对象分页列表
     */
    public Page<SysUser> selectPageListByRoleId(String roleId) {
        return userDao.selectPageListByRoleId(roleId);
    }

    /**
     * 刷新用户缓存
     */
    public void refreshCache() {
        userCache.clear();
        List<SysUser> list = userDao.selectList(new UserFilter());
        for (SysUser entity : list) {
            userCache.put(entity.getId(), entity);
        }
        log.info("刷新系统用户缓存");
    }

    /**
     * 保存用户
     * @param entity  用户对象
     * @param created 是否新建
     */
    private RestMessage save(SysUser entity, boolean created) {
        //检测密码
        if (created && Helper.isBlank(entity.getPassword())) {
            return RestMessage.fail("请指定用户密码");
        }
        //检测密码强度
        if (created) {
            RestMessage strongResult = SecurityHelper.validPwdStrong(entity.getPassword());
            if (strongResult.isFail()) {
                return strongResult;
            }
        }

        //自动添加主键
        if (created && Helper.isBlank(entity.getId())) {
            entity.setId(Helper.nextIdStr());
        }
        //添加简拼
        if (Helper.isBlank(entity.getSpell())) {
            entity.setSpell(Helper.getSpell(entity.getName()));
        }

        var currentUser = getCurrentUser();
        if (!created && !currentUser.getIsAdmin() && entity.getId().equals(currentUser.getId())) {//修改数据时,当前用户不能修改自己的信息,只有上级账号才能修改(超管除外)
            return RestMessage.fail("修改用户[" + entity.getAccount() + "]失败，当前用户不能修改自身信息,只有上级机构账号才能修改");
        }

        //只有当前用户是管理员才有权限设置用户的管理员状态
        entity.setIsAdmin(getCurrentUser().getIsAdmin() && entity.getIsAdmin());

        //如果密码不过期,则清空密码过期时间
        if (entity.getPwdNeverExpire()) {
            entity.setPwdExpireTime(null);
        }

        //新建并且密码过期,设置过期时间
        if (created && !entity.getPwdNeverExpire()) {
            entity.setPwdExpireTime(DateUtil.offsetDay(new Date(), Sys.setting().getApp().getSysUserPwdAvailableDays()));
        }

        //账号验证
        if (userDao.existByAccount(entity.getAccount(), entity.getId())) {
            return RestMessage.fail(Helper.format("用户账号[{}]已经存在", entity.getAccount()));
        }

        if (created) {// 新建
            entity.setLoginCount(0L);
            entity.setPwdSalt(SecurityHelper.createSalt());
            entity.setPassword(SecurityHelper.encryptPassword(entity.getAccount(), entity.getPassword(), entity.getPwdSalt()));
            userDao.insert(entity);//插入用户
            saveInsertHistory(HistoryInfo.builder().pk(entity.getId()).after(userDao.selectById(entity.getId())).build());//记录历史记录
        } else {     //修改
            var old = userDao.selectById(entity.getId());
            //判断账号是否修改,由于账号和密码有绑定关系,如果账号被修改则无法登陆,所以不允许修改账号
            if (!old.getAccount().equals(entity.getAccount())) {
                throw new AppException("由于账号和密码有绑定关系,所以不允许修改账号");
            }
            userDao.update(entity); //更新用户
            saveUpdateHistory(HistoryInfo.builder().pk(entity.getId()).before(old).after(userDao.selectById(entity.getId())).build());
        }
        userCache.evict(entity.getId());
        return RestMessage.success();
    }

    private TimerTask saveLoginLog(String account, boolean status, String msg) {
        var loginLog = Sys.me().buildLoginLog(account, status, msg);
        return new TimerTask() {
            @Override
            public void run() {
                loginLogService.insert(loginLog);
            }
        };
    }

    // endregion
}